Skip to content

Skip tautological evaluators in MessageEvaluator.append#454

Merged
pkwarren merged 2 commits intobufbuild:mainfrom
snuderl:perf/message-evaluator-tautology-filter
Apr 23, 2026
Merged

Skip tautological evaluators in MessageEvaluator.append#454
pkwarren merged 2 commits intobufbuild:mainfrom
snuderl:perf/message-evaluator-tautology-filter

Conversation

@snuderl
Copy link
Copy Markdown
Contributor

@snuderl snuderl commented Apr 23, 2026

Summary

MessageEvaluator.append stored every evaluator unconditionally, so FieldEvaluators for fields with no rules ran on every validate() call: hasField check, ObjectValue allocation, ValueEvaluator.evaluate with a zero-length evaluator list, return empty. None of that can ever produce a violation, so iterating those entries is pure overhead.

ValueEvaluator.append already filters on tautology(); this mirrors the check in MessageEvaluator.append to match protovalidate-go's AppendNested (internal/evaluator/message.go:67-77), which skips appends whose Tautology() is true.

Impact

Measured on a message with 2 fields (1 ruled, 1 unruled) over 1M iterations with a warm JIT:

  • baseline: ~977 ns/validate
  • with filter: ~908 ns/validate (~7%)

The effect scales with the fraction of no-rule fields. Messages with many unconstrained fields (common in real-world proto definitions where validation targets a subset) save proportionally more.

Test plan

  • ./gradlew test passes (unit + conformance)

MessageEvaluator.append previously stored every evaluator unconditionally,
so FieldEvaluators for fields with no rules still ran on every validate()
call: hasField check, ObjectValue allocation, ValueEvaluator.evaluate
with a zero-length evaluator list, return empty. None of that can ever
produce a violation, so iterating them is pure overhead.

ValueEvaluator.append already filters on tautology(); this change mirrors
that in MessageEvaluator to match protovalidate-go's AppendNested
(internal/evaluator/message.go:67-77), which skips appends whose
Tautology() is true.

The effect scales with the fraction of no-rule fields in a message. On
a 2-field message (1 ruled, 1 unruled), per-call validate() dropped
from ~977 ns to ~908 ns (~7%). Messages with more unconstrained fields
save proportionally more.
pkwarren added a commit that referenced this pull request Apr 23, 2026
In order to verify performance fixes (like #451 and #454), it would be
helpful to be able to write a JMH benchmark and quickly measure the
impact.

This adds a benchmark module to the Gradle build. CI only verifies the
build of the benchmarks. Added a README showing how to add benchmarks
and measure improvements locally.

Instead of outputting JSON or other format, created a jq script to
roughly print out output similar to benchstat for Go.
pkwarren added a commit that referenced this pull request Apr 23, 2026
In order to verify performance fixes (like #451 and #454), it would be
helpful to be able to write a JMH benchmark and quickly measure the
impact.

This adds a benchmark module to the Gradle build. CI only verifies the
build of the benchmarks. Added a README showing how to add benchmarks
and measure improvements locally.

Instead of outputting JSON or other format, created a jq script to
roughly print out output similar to benchstat for Go.
Copy link
Copy Markdown
Member

@pkwarren pkwarren left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes look good to me. Thanks for the fix!

> Task :benchmarks:jmhCompare
benchmark                    metric  before            after             delta
validateManyUnruled          time    625.57 ns/op      407.94 ns/op      -34.8%
validateManyUnruled          alloc   3140 B/op         2088 B/op         -33.5%

@pkwarren pkwarren merged commit 05fa7e3 into bufbuild:main Apr 23, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants